﻿/****************************************************************************************
 *
 *  Filename:    LogSubstPol.cs
 *  Description: TFS Check-In Policy for keyword expansion
 *  License: GPLv2: http://logsubstpol.codeplex.com/license
 *
 *  History
 *  -------
 *
 *    Date   | Author          | Comment
 *  ---------+-----------------+---------------------------------------------------------
 *  08.08.09 | jkalmbach_cp    | First version published on
 *           |                 | Codeplex: http://logsubstpol.codeplex.com/
 *  09.08.09 | jkalmbach_cp    | Keyword substitition implemented
 *  10.01.10 | jkalmbach_cp    | Changed to correctly set file size when keyword expansion 
 *           |                 | reduces the file size (Path #4879 from wheeley)
 *  $log$
 *
 ****************************************************************************************
 */
using System;
using System.Windows.Forms;
using System.Collections.Generic;
using Microsoft.TeamFoundation.VersionControl.Client;
using System.IO;
using System.Xml.Serialization;

namespace LogSubstPol
{
  #region LogSubstPol
  [Serializable]
  public class LogSubstPol : PolicyBase
  {
    #region Fields (Configuration / consts)

    [NonSerialized]
    const int _MaxFileSize = 1 * 1024 * 1024;  // 1 MB
    [NonSerialized]
    const int _MaxLogSize = 1024;  // 1 KB

    [NonSerialized]
    const string teamSettingsFilePath = "$/{0}/TeamProjectConfig/CheckinPolicies";
    [NonSerialized]
    const string teamSettingsFileName = "LogSubstPolSetting.xml";

    [NonSerialized]
    const char keyWordStartEndTag = '$';
    [NonSerialized]
    const string keyWordTextSep = ": ";

    KeywordList _Keywords;
    KeywordList Keywords
    {
      get
      {
        if (_Keywords == null)
        {
          _Keywords = new KeywordList();
          _Keywords.Add(new Keyword() { Key = "Author", Format = "{5}" });
          _Keywords.Add(new Keyword() { Key = "Source", Format = "{8}" });
          _Keywords.Add(new Keyword() { Key = "Date", Format = "{1:yyyy}/{1:MM}/{1:dd} {1:HH:mm:ss}" });
          _Keywords.Add(new Keyword() { Key = "Header", Format = "{8}, {6}+1 {1:yyyy}/{1:MM}/{1:dd} {1:HH:mm:ss} {5}" });
          _Keywords.Add(new Keyword() { Key = "Id", Format = "{9}, {6}+1 {1:yyyy}/{1:MM}/{1:dd} {1:HH:mm:ss} {5}" });
          _Keywords.Add(new Keyword() { Key = "RCSFile", Format = "{9}" });
          _Keywords.Add(new Keyword() { Key = "PrevRevision", Format = "{6}" });
          _Keywords.Add(new Keyword() { Key = "Revision", Format = "{6}+1" });
        }
        return _Keywords;
      }
    }


    /// <summary>
    /// Wenn dies <c>true</c> ist, dann soll *vor* der $log$-Zeile der Kommentar eingefügt werden (neueste Einträge unten)
    /// Per default (<c>false</c>) wird er immer nach der $log$-Zeile eingefügt (neueste Einträge oben)
    /// </summary>
    internal bool _InsertBeforeLogLine = false;
    string[] _Extensions;
    string[] _FirstLines;
    string[] _RepeatLines;
    string[] _EndLines;
    internal UserNameMappingListe _UserNameMappings;
    internal bool CheckForNewConfig;


    internal string[] Extensions
    {
      get
      {
        if (_Extensions == null)
          _Extensions = new string[]
          {
            ".cs", 
            ".c", ".cpp", ".cxx", ".h", ".hpp", ".hxx", ".inc", ".dcl",
            ".asp", ".aspx", 
            ".xml", ".htm", ".html",
            ".xaml",
            //".rc", ".resx",
          };
        return _Extensions;
      }
      set
      {
        _Extensions = value;
      }
    }

    internal string[] FirstLines
    {
      get
      {
        if (_FirstLines == null)
          _FirstLines = new string[]
          {
            // 0: Current DateTime (local)
            // 1: Current DateTime (UTC)
            // 2: Long UserName (with domain-prefix)
            // 3: Short UserName (without domain-prefix)
            // 4: ChangeTypeName (for example "add, edit")
            // 5: Mapped UserName (or Short UserName if no mapping was found)
            // 6: Previous-Revision number (ChangeSet-ID)
            // 7: Previous-Revision number (ChangeSet-ID) + 1
            //"{0:yyyy-MM-dd HH:mm:ss}, {1}, {2}, {3}: $COMMENT$",
            "Revision {7} {0:yyyy}/{0:MM}/{0:dd} {0:HH:mm:ss}  {5}",
          };
        return _FirstLines;
      }
      set
      {
        _FirstLines = value;
      }
    }

    internal string[] RepeatLines
    {
      get
      {
        if (_RepeatLines == null)
          _RepeatLines = new string[]
          {
            "  $COMMENT$",
          };
        return _RepeatLines;
      }
      set
      {
        _RepeatLines = value;
      }
    }

    internal string[] EndLines
    {
      get
      {
        if (_EndLines == null)
          _EndLines = new string[]
          {
          };
        return _EndLines;
      }
      set
      {
        _EndLines = value;
      }
    }

    [NonSerialized]
    static byte[] _Buffer;

    #endregion

    #region IPolicyDefinition
    public override string Description
    {
      get
      {
        return "Log Substitution Policy: Automatically inserts the check-in comment into the source-file; of course, this only works if you have provided a comment";
      }
    }

    // This is a string that is stored with the policy definition on the source
    // control server. If a user does not have the policy plug-in installed, this string
    // is displayed.  You can use this to explain to the user how they should 
    // install the policy plug-in.
    public override string InstallationInstructions
    {
      get { return "Log Substitution Policy: To install this policy, read InstallInstructions.txt."; }
    }

    // This string identifies the type of policy. It is displayed in the 
    // policy list when you add a new policy to a Team Project.
    public override string Type
    {
      get { return "Log Substitution Policy: Inserts Comments on Check-In"; }
    }

    // This string is a description of the type of policy. It is displayed 
    // when you select the policy in the Add Check-in Policy dialog box.
    public override string TypeDescription
    {
      get { return "Log Substitution Policy: This policy will prompt the user to decide whether or not they should be allowed to check in."; }
    }


    public override bool CanEdit
    {
      get
      {
        return true;
      }
    }
    // This method is called by the policy framework when you create 
    // a new check-in policy or edit an existing check-in policy.
    // You can use this to display a UI specific to this policy type 
    // allowing the user to change the parameters of the policy.
    public override bool Edit(IPolicyEditArgs args)
    {
      VerifySettings(args.TeamProject, true);
      // Do not need any custom configuration
      using (CommentSettingDlg dlg = new CommentSettingDlg())
      {
        IWin32Window parent = null;
        if (args != null)
          parent = args.Parent;
        dlg.FirstLines = this.FirstLines;
        dlg.RepeatLines = this.RepeatLines;
        dlg.EndLines = this.EndLines;
        dlg.Extensions = this.Extensions;
        dlg.InsertBefore = this._InsertBeforeLogLine;
        dlg.UserNameMappings = this._UserNameMappings;
        dlg.CheckForNewConfig = this.CheckForNewConfig;
        if (dlg.ShowDialog(parent) == DialogResult.OK)
        {
          this._FirstLines = dlg.FirstLines;
          this._RepeatLines = dlg.RepeatLines;
          this._EndLines = dlg.EndLines;
          this._Extensions = dlg.Extensions;
          this._InsertBeforeLogLine = dlg.InsertBefore;
          this._UserNameMappings = dlg.UserNameMappings;
          this.CheckForNewConfig = dlg.CheckForNewConfig;

          // Speichere es in den TeamProjectConfig-Settings diese Einsstellungen...
          if (args != null)
            this.Save(args.TeamProject);
        }
      }
      return true;
    }
    #endregion

    #region IPolicyEvaluation
    // This method performs the actual policy evaluation. 
    // It is called by the policy framework at various points in time
    // when policy should be evaluated. In this example, the method 
    // is invoked when various asyc events occur that may have 
    // invalidated the current list of failures.
    public override PolicyFailure[] Evaluate()
    {
      string proposedComment = PendingCheckin.PendingChanges.Comment;
      List<PolicyFailure> pfList = new List<PolicyFailure>();
      if (String.IsNullOrEmpty(proposedComment))
      {
        pfList.Add(new PolicyFailure("Log Substitution Policy: You must provide a comment", this));
      }
      else
      {
        bool bFirst = true;
        PendingChange[] pendingChanges = PendingCheckin.PendingChanges.CheckedPendingChanges;

        // TODO: sort all peding changes by TeamProject...
        // Then check-in the changes according to the TeamProject-Setting...
        foreach (PendingChange pc in pendingChanges)
        {
          if (bFirst)
          {
            // TODO: Currently we use the settings from the *first* item!
            // We should use the settings for each team-project if the items are in different projects...
            try
            {
              TeamProject tp = pc.VersionControlServer.TryGetTeamProjectForServerPath(pc.ServerItem);
              if (tp != null)
                VerifySettings(tp, false);
            }
            catch (Exception exp)
            {
              System.Diagnostics.Trace.WriteLine(exp);
            }
            bFirst = false;
          }
          // Schaue nach, ob es eine "Edit-Änderung ist
          if ((IsVerifyableChangeType(pc.ChangeType)) && (IsVerifyableFileType(pc.LocalItem)))
          {
            System.Diagnostics.Trace.WriteLine("Log Substitution Policy: Local item: " + pc.LocalItem + " change type: " + pc.ChangeType.ToString());

            try
            {
              // TODO: First check, if the Has-Value is the same.... (if the file was really changed!)
              //if(pc.HashValue != GetHashValueFromFile(pc).LocalItem)
                FillCommentInSourceFile(proposedComment, pc.LocalItem, pc);
            }
            catch (System.Exception exp)
            {
              System.Diagnostics.Trace.WriteLine(exp);
            }

          }
        }
      }
      return pfList.ToArray();
    }

    // This method is called if the user double-clicks on 
    // a policy failure in the UI. In this case a message telling the user 
    // to supply some comments is displayed.
    public override void Activate(PolicyFailure failure)
    {
      MessageBox.Show("Log Substitution Policy: Please provide comments for your check-in.", "How to fix your policy failure");
    }

    // This method is called if the user presses F1 when a policy failure 
    // is active in the UI. In this example, a message box is displayed.
    public override void DisplayHelp(PolicyFailure failure)
    {
      MessageBox.Show("Log Substitution Policy: This policy helps you to remember to add comments to your check-ins.", "Prompt Policy Help");
    }
    #endregion

    #region Encoding support
    System.Text.Encoding DetectEncoding(byte[] buffer, int len)
    {
      System.Text.Encoding enc = System.Text.Encoding.Default;
      if (buffer == null) return enc;
      // Now find out the first time Encoding:
      // I'm currently only using the BOM
      // If no BOM is present, use default encoding
      if (len > 3)
      {
        if ((buffer[0] == 0xEF) && (buffer[1] == 0xBB) && (buffer[2] == 0xBF))
        {
          enc = System.Text.Encoding.UTF8;
        }
        // Everything else is just not supported yet ...
        // FF FE UTF-16, little-endian
        // FE FF UTF-16, big-endian
        // FF FE 00 00 UTF-32 little-endian
        // 00 00 FE FF UTF-32, big-endian (.NET has no support yet ;))
      }
      return enc;
    }
    #endregion

    #region Text detect/insert support
    [NonSerialized]
    byte[] _CR;
    [NonSerialized]
    byte[] _LF;
    /// <summary>
    /// Returns the (start) position of the next CR/LF or -1 if not found (EOF)
    /// </summary>
    /// <param name="buffer"></param>
    /// <param name="startIdx"></param>
    /// <param name="len"></param>
    /// <returns></returns>
    int FindNextCRLF(byte[] buffer, int startIdx, int len, System.Text.Encoding enc)
    {
      System.Diagnostics.Debug.Assert(enc != null);
      System.Diagnostics.Debug.Assert(buffer != null);
      System.Diagnostics.Debug.Assert(startIdx >= 0);
      System.Diagnostics.Debug.Assert(len >= 0);
      System.Diagnostics.Debug.Assert(buffer.Length >= startIdx + len);

      if (_CR == null) _CR = enc.GetBytes("\n");
      if (_LF == null) _LF = enc.GetBytes("\r");

      int crIdx = this.IndexOf(buffer, startIdx, len, _CR);
      int lfIdx = this.IndexOf(buffer, startIdx, len, _LF);
      if ((crIdx >= 0) && (lfIdx >= 0))
        return Math.Min(crIdx, lfIdx);
      if (crIdx >= 0) return crIdx;
      return lfIdx;
    }

    /// <summary>
    /// This function inserts the keytext in the keyword and returns the new buffer length in len
    /// </summary>
    /// <param name="buffer"></param>
    /// <param name="len"></param>
    /// <param name="maxLen"></param>
    /// <param name="keyword"></param>
    /// <param name="keytext"></param>
    /// <param name="enc"></param>
    /// <returns></returns>
    bool ReplaceKeyword(byte[] buffer, ref int len, int maxLen, string keyword, string keytext, System.Text.Encoding enc, out int changeStartPos)
    {
      changeStartPos = 0;
      if (keyword == null) return false;
      if (keyword.Length <= 0) return false;
      if (keytext == null) return false;
      byte[] keywordBytes = enc.GetBytes(keyWordStartEndTag + keyword);
      int keywordStartIdx = this.IndexOf(buffer, 0, len, keywordBytes);
      if (keywordStartIdx < 0) return false;

      // replace our end-tag with an other character...
      keytext = keytext.Replace('$', '?');

      // find the ending "$":
      byte[] keywordEndTag = enc.GetBytes(new char[] {keyWordStartEndTag});
      int keywordTextStartIdx = keywordStartIdx + keywordBytes.Length;
      int endIdx = this.IndexOf(buffer, keywordTextStartIdx, len - keywordTextStartIdx, keywordEndTag);
      if (endIdx < 0) return false;  // no end Idx found...
      int lineEndIdx = this.FindNextCRLF(buffer, keywordStartIdx, len - keywordStartIdx, enc);
      if ((lineEndIdx>= 0) && (lineEndIdx < endIdx)) return false; // the end tag was found, but on an other line, so ignore it...

      // Now we have found the keyword with the starting tag ($) and the ending tag ($)
      int prevKeywordTextLen = endIdx - keywordTextStartIdx;
      if (prevKeywordTextLen > 0)
      {
        // Some text was alreay inserted... check if it starts with ": ";
        int sIdx = IndexOf(buffer, keywordTextStartIdx, len - keywordTextStartIdx, enc.GetBytes(keyWordTextSep));
        if (sIdx != keywordTextStartIdx)
          return false;  // The keyword was not in the correctly format (missing ": ")
      }

      // Sodelle... jetzt füge den neuen Text ein...
      byte[] text2Insert = enc.GetBytes(keyWordTextSep + keytext + " ");
      if (prevKeywordTextLen < text2Insert.Length)
      {
        // Make some room in the array...
        int neededBytes = text2Insert.Length - prevKeywordTextLen;
        if (len + neededBytes > maxLen) return false;  // The buffer size is too small...
        for (int i = len; i >= keywordTextStartIdx; i--)
          buffer[i + neededBytes] = buffer[i];
        // actualize the len:
        len += neededBytes;
      }
      else if (prevKeywordTextLen > text2Insert.Length)
      {
        // Remove the unused space...
        int unusedBytes = prevKeywordTextLen - text2Insert.Length;
        for (int i = keywordTextStartIdx + text2Insert.Length; i < len; i++)
          buffer[i] = buffer[i + unusedBytes];
        // actualize the len:
        len -= unusedBytes;
      }
      else
      {
        // the length is the same... check if something has changed...
        bool bSame = true;
        for (int i = 0; i < text2Insert.Length; i++)
        {
          if (buffer[keywordTextStartIdx + i] != text2Insert[i])
          {
            bSame = false;
            break;
          }
        }
        if (bSame)
          return false; // Nothing has changed... so no need to save or modify the file...
      }

      // Now we can insert the text...
      Array.Copy(text2Insert, 0, buffer, keywordTextStartIdx, text2Insert.Length);

      changeStartPos = keywordTextStartIdx;

      return true;
    }
    #endregion

    #region Comment insertion
    public void FillCommentInSourceFile(string comment, string fileName, PendingChange pc)
    {
      if (System.IO.File.Exists(fileName) == false) return;
      // reset _LF/_CR, because every file can has its own encoding...
      _LF = null;
      _CR = null;
      using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
      {
        if (fs.Length > _MaxFileSize)
        {
          System.Diagnostics.Trace.WriteLine("Log Substitution Policy: Local item: " + fileName + " is too big (" + fs.Length.ToString() + " Bytes); ignoring.");
          return;  // If the file is larger than 5 MB, then I can not actually edit them ...
        }
        int len = (int)fs.Length;
        int orgLen = len;

        if (_Buffer == null)  // allocate this only * once *!
          _Buffer = new byte[_MaxFileSize + _MaxLogSize];


        // Read the whole file:
        fs.Read(_Buffer, 0, len);

        System.Text.Encoding enc = DetectEncoding(_Buffer, len);


        FormatArgs args = new FormatArgs(pc, this);

        // Keyword substitution...
        bool bFileMustBeSaved = false;
        int saveStartPos = int.MaxValue;

        // Keywords
        foreach (var k in Keywords)
        {
          int csp;
          if (ReplaceKeyword(_Buffer, ref len, _MaxFileSize + _MaxLogSize, k.Key, GetFormattedString(k.Format, args), enc, out csp))
          {
            saveStartPos = Math.Min(saveStartPos, csp);
            bFileMustBeSaved = true;
          }
        }


        // Sodelle... now look for a place where a "$log$" occurs ...
        byte[] prefix;
        int insertIdx = FindLogIdx(_Buffer, len, out prefix, enc);
        if (insertIdx >= 0)
        {
          // Find the CR/LF-Typ:
          byte[] newLine = GetNewLineChars(_Buffer, len, enc);

          // Now the comment should be added....
          // Create it for now...
          string[] al = GetCommentLines(comment, args);

          List<byte> addBytes = new List<byte>();
          //addBytes.AddRange(prefix);
          //addBytes.AddRange(newLine);
          foreach (string s in al)
          {
            byte[] b = enc.GetBytes(s);
            addBytes.AddRange(prefix);
            addBytes.AddRange(b);
            addBytes.AddRange(newLine);
          }

          // So, füge jetzt das ganze ein, wenn es reinpasst:
          if ((len + addBytes.Count) > (_MaxFileSize + _MaxLogSize))
          {
            System.Diagnostics.Trace.WriteLine("Log Substitution Policy: Comment is too long (" + addBytes.Count.ToString() + " Bytes); ignoring.");
            return;
          }

          // Make space in the buffer (move the data)
          //Array.Copy(_Buffer, insertIdx, _Buffer, insertIdx + addBytes.Count, addBytes.Count);  // not at all possible...
          int addCnt = addBytes.Count;
          for (int i = len; i >= insertIdx; i--)
            _Buffer[i + addCnt] = _Buffer[i];
          // Add it now
          Array.Copy(addBytes.ToArray(), 0, _Buffer, insertIdx, addCnt);
          len += addCnt;
          bFileMustBeSaved = true;
          saveStartPos = Math.Min(saveStartPos, insertIdx);
        }

        if (bFileMustBeSaved == true)
        {
          // And write the file out from this place again
          fs.Seek(saveStartPos, SeekOrigin.Begin);
          fs.Write(_Buffer, saveStartPos, len - saveStartPos);
          if (len < orgLen)
              fs.SetLength(len);
        }
      }  // using
    }

    /// <summary>
    /// Returns -1 if the index was not found or the position at which the comment should be inserted
    /// </summary>
    /// <param name="buffer"></param>
    /// <returns></returns>
    int FindLogIdx(byte[] buffer, int len, out byte[] prefix, System.Text.Encoding enc)
    {
      prefix = null;
      // INFO: Here you can Add various log-patterns if you like... currently three are supported...
      string[] logPatterns = { "$log$", "$Log$", "$LOG$" };
      byte[][] patterns = new byte[logPatterns.Length][];

      // initialize all patterns with the corresponding bytes from the codepage
      for(int i=0; i<logPatterns.Length; i++)
      {
        patterns[i] = enc.GetBytes(logPatterns[i]);
      }

      int startIdx = 0;
      int idx = -1;

      // TODO: Use Encoding...
      //if (_CR == null) _CR = enc.GetBytes("\n");
      //if (_LF == null) _LF = enc.GetBytes("\r");

      do
      {
        bool bFound = false;
        bool nothingFound = true;
        foreach(byte[] pattern in patterns)
        {
          idx = IndexOf(buffer, startIdx, len - startIdx, pattern);
          if (idx >= 0)
          {
            nothingFound = false;
            // check now, if it realy ends with CR/LF...
            if (len > idx + pattern.Length)
            {
              if ((buffer[idx + pattern.Length] == '\n') || (buffer[idx + pattern.Length] == '\r'))
              {
                // It ends with CR/LF; this MUST BE THE CASE in order to use the "$log$" keyword!
                bFound = true;
                break;
              }
              startIdx = idx + 1;
            }
            else
            {
              // It is the last line in the buffer, so accept it...
              bFound = true;
              break;
            }
          }
        }  // foreach
        
        if (nothingFound)
          return -1;  // I have nothing found... so we can stop searching...
        if (bFound)
          break;  // we have found a matching log-line with CR/LF at the end...
      } while (true);

      // Now search for the beginning of the line
      // All bytes from the beginning of the line until the log-start will be used as "prefix" (e.g. always use "// ")
      // TODO: Encoding support!
      int previousCR = Array.LastIndexOf<byte>(buffer, (byte)'\n', idx, idx + 1);
      if (previousCR < 0)
        previousCR = 0;
      else
        previousCR++;

      prefix = new byte[idx - previousCR];
      Array.Copy(buffer, previousCR, prefix, 0, prefix.Length);

      if (this._InsertBeforeLogLine)
      {
        // Search for optional '\r'
        // TODO: Encoding support!
        if ((previousCR > 0) && (buffer[previousCR - 1] == (byte)'\r'))
          return previousCR - 1;
        return previousCR;
      }

      // Search for next CR:
      // TODO: Encoding support!
      int newLineIdx = Array.IndexOf<byte>(buffer, (byte)'\n', idx, len - idx);
      if (newLineIdx < 0)
        newLineIdx = len;
      else
        newLineIdx++;

      // Now return the "insert-position-Index"...
      return newLineIdx;
    }

    int IndexOf(byte[] buffer, int startIdx, int len, byte[] pattern)
    {
      if (len < pattern.Length) return -1;
      for (int i = startIdx; i <= (startIdx + len) - pattern.Length; i++)
      {
        bool found = true;
        for (int j = 0; j < pattern.Length; j++)
        {
          if (buffer[i + j] != pattern[j])
          {
            found = false;
            break;
          }
        }
        if (found)
        {
          return i;
        }
      }
      return -1;
    }

    byte[] GetNewLineChars(byte[] buffer, int len, System.Text.Encoding enc)
    {
      // TODO: Use Encoding...
      for (int i = 0; i < len; i++)
      {
        if (buffer[i] == '\n')
        {
          if ((i > 0) && (buffer[i - 1] == '\r'))
            return new byte[] { (byte)'\r', (byte)'\n' };
          return new byte[] { (byte)'\n' };
        }
      }
      return new byte[] { (byte)'\n' };
    }


    string[] GetCommentLines(string comment, FormatArgs args)
    {
      comment = comment.Replace("\r", "");
      string[] clines = comment.Split('\n');

      System.Collections.ArrayList al = new System.Collections.ArrayList();
      // gehe jetzt zuerst mal alle "FirstLines" durch...
      int commentIdx = 0;
      foreach (string s in FirstLines)
      {
        string c = GetFormattedString(s, args);
        // schaue jetzt noch ob ich einen Kommentar einfügen soll:
        InsertPossibleComment(ref c, clines, ref commentIdx);
        al.Add(c);
      }

      // Die "for-schleife" ist eigentlich nur für den Notfall; falls in den Repeat-Lines gar kein $COMMENT$ drinj ist!
      for (int cidx = commentIdx; cidx < clines.Length; cidx++)
      {
        foreach (string s in RepeatLines)
        {
          string c = GetFormattedString(s, args);
          // schaue jetzt noch ob ich einen Kommentar einfügen soll:
          InsertPossibleComment(ref c, clines, ref commentIdx);
          al.Add(c);
        }
        // Hier wird wirklich abgebrochen (wenn ein $COMMENT$ drin ist, was ja der Fall sein muss!
        if (commentIdx >= clines.Length)
          break;
      }

      foreach (string s in EndLines)
      {
        string c = GetFormattedString(s, args);
        al.Add(c);
      }

      return (string[])al.ToArray(typeof(string));
    }

    void InsertPossibleComment(ref string c, string[] clines, ref int commentIdx)
    {
      // Ersetze dann das $COMMENT:...$" durch {0:...}" //somit kann man hier auch Formatierungs-Strings angeben...
      int cStartIdx = c.IndexOf("$COMMENT");
      int cEndIdx = 0;
      if (cStartIdx >= 0)
        cEndIdx = c.IndexOf("$", cStartIdx + 1);
      if ((cStartIdx >= 0) && (cEndIdx > cStartIdx))
      {
        string cmt = string.Empty;
        if (commentIdx < clines.Length)
        {
          // Jetzt hab ich was gefunden...
          cmt = clines[commentIdx];
          commentIdx++;
        }
        string fmtAdd = c.Substring(cStartIdx + 8, cEndIdx - cStartIdx - 8);
        if (fmtAdd.Length > 0)
        {
          string fmt = "{0" + fmtAdd + "}";
          try
          {
            StringShortenFormatter _MyFormatter = new StringShortenFormatter();
            cmt = string.Format(_MyFormatter, fmt, cmt);
          }
          catch (System.Exception exp)
          {
            System.Diagnostics.Trace.WriteLine("Log Substitution Policy: Exception formating comment string '" + fmt + "'! " + exp.ToString());
          }
        }
        // So, baue jetzt die ganze Zeile zusammen...
        c = c.Substring(0, cStartIdx) + cmt + c.Substring(cEndIdx + 1);
      }
    }

    string GetFormattedString(string format, FormatArgs args)
    {
      string result = null;
      try
      {
        StringShortenFormatter _MyFormatter = new StringShortenFormatter();
        result = string.Format(_MyFormatter, format, args.nowLocal, args.nowUtc, args.fullUserName, args.shortUserName, args.changeName, args.mappedUserName, args.previousVersion, args.previousVersionPlusOne, args.serverItem, args.fileName);
      }
      catch (System.Exception exp)
      {
        System.Diagnostics.Trace.WriteLine("Log Substitution Policy: Exception formating string '" + format + "'! " + exp.ToString());
      }
      if (result == null) return format;
      return result;
    }
    #endregion

    #region Check for correct change-types and file extensions
    bool IsVerifyableChangeType(ChangeType ct)
    {
      if ((ChangeType.Add == (ct & ChangeType.Add)) || (ChangeType.Edit == (ct & ChangeType.Edit)))
      {
        return true;
      }
      return false;
    }

    private bool IsVerifyableFileType(string fileName)
    {
      string extension = Path.GetExtension(fileName).ToLowerInvariant();
      foreach (string s in Extensions)
        if (s == extension)
          return true;

      return false;
    }
    #endregion

    #region Setting management
    [NonSerialized]
    LogSubstPolSetting _LastSettings;
    void VerifySettings(TeamProject args, bool forceLoad)
    {
      try
      {
        if (_LastSettings == null)
          _LastSettings = Load(args, null);
        else
        {
          if ( (string.Compare(_LastSettings.TeamProjectName, args.Name, true) == 0)  && (forceLoad == false) )
          {
            if (_LastSettings.CheckForNewConfig)
              _LastSettings = Load(args, _LastSettings);
          }
          else
            _LastSettings = Load(args, null);
        }
        if (_LastSettings != null)
          _LastSettings.Set(this);
      }
      catch(Exception exp)
      {
        System.Diagnostics.Debug.WriteLine(exp);
      }
    }
    #endregion

    #region Load/Save Settings in TFS "teamSettingsFile"
    public LogSubstPolSetting Load(TeamProject args, LogSubstPolSetting cachedSetting)
    {
      try
      {
        if (args == null) return cachedSetting;
        string item = string.Format(teamSettingsFilePath, args.Name) + "/" + teamSettingsFileName;
        var i = args.VersionControlServer.GetItem(item, VersionSpec.Latest);
        if ((cachedSetting == null) || (i.ChangesetId != cachedSetting.ChangesetId))
        {
          Stream s = i.DownloadFile();
          var sr = new System.IO.StreamReader(s);
          // Load the values from the stream...
          LogSubstPolSetting settings = LogSubstPolSetting.Load(sr);
          TeamProject tp = i.VersionControlServer.TryGetTeamProjectForServerPath(i.ServerItem);
          if (tp != null)
            settings.TeamProjectName = tp.Name;
          settings.ServerItem = i.ServerItem;
          settings.ChangesetId = i.ChangesetId;
          return settings;
        }
      }
      catch (System.Exception exp)
      {
        System.Diagnostics.Debug.WriteLine(exp);
      }
      return cachedSetting;
    }

    public bool Save(TeamProject args)
    {
      if (args == null) return false;
      string serverPath = string.Format(teamSettingsFilePath, args.Name);
      string item = serverPath + "/" + teamSettingsFileName;
      Item i = null;
      WorkingFolder selfMapped = null;
      try
      {
        i = args.VersionControlServer.GetItem(item);
      }
      catch(Exception exp)
      {
        System.Diagnostics.Debug.WriteLine(exp);
        return false;
      }

      var wss = args.VersionControlServer.QueryWorkspaces(null, args.VersionControlServer.AuthorizedUser, Environment.MachineName);
      Workspace ws = null;
      if ((wss == null) || (wss.Length <= 0))
      {
        ws = args.VersionControlServer.CreateWorkspace(Environment.MachineName, args.VersionControlServer.AuthorizedUser);
      }
      else
      {
        foreach (var w in wss)
        {
          if (w.IsServerPathMapped(serverPath))
          {
            ws = w;
            break;
          }
        }
        if (ws == null)
        {
          ws = wss[0];
          string tp = System.IO.Path.GetTempPath();
          tp = Path.Combine(tp, "LogSubstPol-WS");
          if (Directory.Exists(tp) == false)
            Directory.CreateDirectory(tp);
          selfMapped = new WorkingFolder(serverPath, tp);
          ws.CreateMapping(selfMapped);
        }
      }

      // This can be usefull for debugging...
      //args.VersionControlServer.NonFatalError += new ExceptionEventHandler(VersionControlServer_NonFatalError);
      try
      {
        ws.Get(new string[] { item }, VersionSpec.Latest, RecursionType.None, GetOptions.Overwrite);
        string fullName = ws.GetLocalItemForServerItem(item);
        int ret = ws.PendEdit(fullName);

        using (StreamWriter sw = new StreamWriter(fullName))
        {
          LogSubstPolSetting pol = new LogSubstPolSetting();
          pol.Fill(this);
          pol.Save(sw);
        }

        var ps = ws.GetPendingChanges(fullName);
        ret = ws.CheckIn(ps, "Log Substitution Policy: Settings changed");

        if (selfMapped != null)
        {
          ws.DeleteMapping(selfMapped);
          try
          {
            File.SetAttributes(fullName, FileAttributes.Normal);
            File.Delete(fullName);
          }
          catch { }
        }

        return true;
      }
      catch (System.Exception exp)
      {
        System.Diagnostics.Debug.WriteLine(exp);
        return false;
      }
    }
    //void VersionControlServer_NonFatalError(object sender, ExceptionEventArgs e)
    //{
    //}
    #endregion

    class FormatArgs
    {
      public FormatArgs(PendingChange pc, LogSubstPol pol)
      {
        fullUserName = string.Empty;
        if (pc != null)
          fullUserName = pc.VersionControlServer.AuthorizedUser;
        shortUserName = fullUserName;
        string[] un = fullUserName.Split('\\');
        if (un.Length == 2)
          shortUserName = un[1];
        changeName = string.Empty;
        previousVersion = -1;
        previousVersionPlusOne = 0;
        if (pc != null)
        {
          changeName = pc.ChangeTypeName;
          previousVersion = pc.Version;
          previousVersionPlusOne = previousVersion + 1;
          serverItem = pc.ServerItem;
          fileName = pc.FileName;
        }

        mappedUserName = shortUserName;
        if (pol._UserNameMappings != null)
        {
          foreach (var um in pol._UserNameMappings)
          {
            if (string.Compare(um.UserName, shortUserName, true) == 0)
            {
              if (um.MappingName != null)
              {
                mappedUserName = um.MappingName;
                break;
              }
            }
          }
        }

        nowLocal = DateTime.Now;
        nowUtc = nowLocal.ToUniversalTime();
      }
      internal DateTime nowLocal;
      internal DateTime nowUtc;
      internal string fullUserName;
      internal string shortUserName;
      internal string changeName;
      internal string mappedUserName;
      internal int previousVersion;
      internal int previousVersionPlusOne;
      internal string serverItem;
      internal string fileName;
    }
  }  // class LogSubstPol
  #endregion

  #region StringShortenFormatter (restricts strings to a maximum number of characters)
  class StringShortenFormatter : IFormatProvider, ICustomFormatter
  {
    // IFormatProvider.GetFormat implementation.
    public object GetFormat(Type formatType)
    {
      // Determine whether custom formatting object is requested.
      if (formatType == typeof(ICustomFormatter))
        return this;
      else
        return null;
    }

    // Format a string with a maximum-length
    public string Format(string format, object arg, IFormatProvider formatProvider)
    {
      // Handle format string.
      // Handle null or empty format string, string with precision specifier.
      string thisFmt = string.Empty;
      string thisFmtArg = string.Empty;
      // Extract first character of format string (precision specifiers
      // are not supported).
      if (!String.IsNullOrEmpty(format))
      {
        thisFmt = format.Length > 1 ? format.Substring(0, 1) : format;
        thisFmtArg = format.Length > 1 ? format.Substring(1) : string.Empty;
      }

      switch (thisFmt.ToUpper())
      {
        // Maximum-Length.
        case "M":
          if (arg is string)
          {
            int i;
            if (int.TryParse(thisFmtArg, out i))
            {
              string a = (string)arg;
              if ((a != null) && (a.Length > i))
                return a.Substring(0, i);
            }
          }
          break;
      }
      // Handle unsupported format strings.
      if (arg is IFormattable)
      {
        string s = ((IFormattable)arg).ToString(format, formatProvider);
        return s;
      }
      else if (arg != null)
        return arg.ToString();
      // arg is null.
      return String.Empty;
    }
  }  // class StringShortenFormatter
  #endregion

  #region LogSubstPolSetting - XML Configuration file
  public class LogSubstPolSetting
  {
    internal static LogSubstPolSetting Load(StreamReader sr)
    {
      XmlSerializer ser = new XmlSerializer(typeof(LogSubstPolSetting));
      return (LogSubstPolSetting)ser.Deserialize(sr);
    }
    internal void Save(StreamWriter sw)
    {
      XmlSerializer ser = new XmlSerializer(typeof(LogSubstPolSetting));
      ser.Serialize(sw, this);
    }

    [XmlIgnore]
    public string TeamProjectName;
    [XmlIgnore]
    public string ServerItem;
    [XmlIgnore]
    public int ChangesetId;

    public bool CheckForNewConfig;
    public bool InsertBeforeLogLine;
    public string[] Extensions;
    public string[] FirstLines;
    public string[] RepeatLines;
    public string[] EndLines;

    public UserNameMappingListe UserNameMappings;

    internal void Set(LogSubstPol pol)
    {
      pol.EndLines = this.EndLines;
      pol.Extensions = this.Extensions;
      pol.FirstLines = this.FirstLines;
      pol._InsertBeforeLogLine = this.InsertBeforeLogLine;
      pol.RepeatLines = this.RepeatLines;
      pol._UserNameMappings = this.UserNameMappings;
      pol.CheckForNewConfig = this.CheckForNewConfig;
    }
    internal void Fill(LogSubstPol pol)
    {
      this.EndLines = pol.EndLines;
      this.Extensions = pol.Extensions;
      this.FirstLines = pol.FirstLines;
      this.InsertBeforeLogLine = pol._InsertBeforeLogLine;
      this.RepeatLines = pol.RepeatLines;
      this.UserNameMappings = pol._UserNameMappings;
      this.CheckForNewConfig = pol.CheckForNewConfig;
    }
  }
  #endregion

  #region UserNameMapping
  [Serializable]
  public class UserNameMapping
  {
    [XmlAttribute("user")]
    public string UserName;
    [XmlAttribute("name")]
    public string MappingName;
  }
  [Serializable]
  public class UserNameMappingListe : List<UserNameMapping> { }
  #endregion

  #region Keywords
  [Serializable]
  public class Keyword
  {
    [XmlAttribute("key")]
    public string Key;
    [XmlAttribute("format")]
    public string Format;
  }
  [Serializable]
  public class KeywordList : List<Keyword> { }
  #endregion
}
